<?php
/**
 * Copyright (c) 2006-2008, Julien PORTALIER
 * http://featherphp.googlecode.com/
 *
 * Licensed under The MIT License
 * Redistributions of files must retain the above copyright notice.
 */

/**
 * URL routing.
 * 
 * \code
 * Routes::connect('', array(':controller' => 'html', ':action' => 'index'));
 * Routes::connect('story/:1/:2', array(':controller' => 'stories', ':action' => 'show',
 *   'requirements' => array(':1' => '\d+', ':2' => '.*')));
 * 
 * Routes::resources('story/:1.:format',
 *   array(':controller' => 'stories', 'requirements' => array(':1' => '\d+')),
 *   array('GET' => 'show', 'POST' => 'add', 'PUT' => 'edit', 'DELETE' => 'delete'));
 * 
 * Routes::resources('story/:1',
 *   array(':controller' => 'stories', 'requirements' => array(':1' => '\d+')),
 *   array('GET' => 'show', 'POST' => 'add', 'PUT' => 'edit', 'DELETE' => 'delete')
 * );
 * 
 * Routes::connect('author/:1', array(':controller' => 'stories', ':action' => 'show'));
 * Routes::connect('login/',    array(':controller' => 'members', ':action' => 'login'));
 * Routes::connect('member/:1', array(':controller' => 'members', ':action' => 'show',
 *   'requirements' => array(':1' => '.*')));
 * \endcode
 * 
 * @todo Handle format automatically (is it possible?)
 * @todo Use memory_cache() instead of file caching (in order to use APC).
 */
class Routes
{
	private static $routes         = array();
	private static $default_routes = array(
		':controller/:action/:id.:format' => array(),
		':controller/:action/:id'         => array(),
		':controller/:action/:id/?:params.:format' => array(),
		':controller/:action/:id/?:params'         => array(),
		':controller/:action.:format' => array(),
		':controller/:action'         => array(),
		':controller.:format' => array(),
		':controller'         => array(),
	);
	private static $default_maps = array(
		':method'     => 'ANY',
		':controller' => '',
		':action'     => 'index',
		':params'     => array(),
		':format'     => 'html',
	);
	private static $default_methods = array(
		'GET'    => 'show',
		'POST'   => 'create',
		'PUT'    => 'update',
		'DELETE' => 'delete',
	);
	private static $transforms = array(
		'/^ANY/'           => '([A-Z]+)',
		'/:controller/'    => '([^/]+)',
		'/:action/'        => '([^/]+)',
		'/:format/'        => '([^./]+)',
#		'/:([\w\_]+)/'     => '([\w\_\-]+)',
		'/:([\w\_]+)/'     => '([^/.]+)',
		'/:params/'        => '(.*?)',
#		'/([\w\_\-]{2,})/' => '(\1)',
	);
	private static $plugins = array();
	public  static $plugin  = null;
	
	
	/**
	 * Ignits the routing sytem.
	 */
	static function init()
	{
		$cache = CACHE.'routes.'.(isset(self::$plugin) ? 'plugin.'.self::$plugin : 'app');
		if (!DEBUG
			and file_exists($cache)
			and filemtime($cache) > filemtime(APP.'config'.DS.'routes.php'))
		{
			# reads routes from cache
			self::$routes = unserialize(file_get_contents($cache));
		}
		else
		{
			# plugin's routes
			if (!empty(Core::$plugin) and file_exists(APP.'plugins'.DS.self::$plugin.DS.'config'.DS.'routes.php')) {
				include APP.'plugins'.DS.self::$plugin.DS.'config'.DS.'routes.php';
			}
			
			# application's routes
			include APP.'config'.DS.'routes.php';
			
			# default routes
			foreach(self::$default_routes as $rte => $maps) {
				self::connect($rte, &$maps);
			}
			
			# caches routes
			file_put_contents($cache, serialize(self::$routes));
		}
	}
	
	/**
	 * Lists installed plugins.
	 */
	static function & list_plugins()
	{
		$plugins = cache('list_of_plugins');
		if (!empty($plugins) and !DEBUG)
		{
			self::$plugins = unserialize($plugins);
		}
		else
		{
			self::$plugins = array();
			if (is_dir(APP.'plugins'))
			{
				if ($dh = opendir(APP.'plugins'))
				{
					while (($fname = readdir($dh)) !== false)
					{
						if (strpos($fname, '.') === false
							and is_dir(APP.'plugins'.DS.$fname))
						{
							self::$plugins[] = $fname;
						}
					}
					closedir($dh);
				}
			}
			cache('list_of_plugins', serialize(self::$plugins));
		}
		return self::$plugins;
	}
	
	/**
	 * Checks if there is a plugin by that name.
	 * 
	 * @return Boolean
	 * @param $name String
	 */
	static function is_plugin($name)
	{
		if (empty(self::$plugins)) {
			self::list_plugins();
		}
		return in_array($name, self::$plugins);
	}
	
	/**
	 * Declares a plugin.
	 *  
	 * @param $name String
	 */
	static function set_plugin($name)
	{
		Routes::$plugin = $name;
		Load::set_plugin($name);
	}
	
	/**
	 * Connects a route to a mapping.
	 * 
	 * \code
	 * Routes::connect('', array(':controller' => 'users', ':action' => 'index'));
	 * Routes::connect('user/:1/:2', array(':controller' => 'users', ':action' => 'show', 'requirements' => array(':2' => '.*'));
	 * \endcode
	 * 
	 * @param $rte String
	 * @param $maps Array
	 */
	static function connect($rte, array $maps)
	{
		$maps = array_merge(self::$default_maps, $maps);
		$rte  = strtoupper($maps[':method']).' '.$rte;
		
		if (!isset(self::$routes[$rte]))
		{
			$maps['regexp'] = self::transform_route($rte, &$maps['requirements']);
			self::$routes[$rte] = $maps;
		}
	}
	
	/**
	 * Connects a REST route to certain mappings.
	 * 
	 * \code
	 * Routes::resources('user/:1.:format', array(':controller' => 'users', ':action' => 'show'));
	 * Routes::resources('user/:1',         array(':controller' => 'users', ':action' => 'show', ':format' => 'html'));
	 * 
	 * Routes::resources('story/:1',
	 * 	array(':controller' => 'stories', 'requirements' => array(':1' => '\d+')),
	 * 	array('GET' => 'show', 'POST' => 'add', 'PUT' => 'edit', 'DELETE' => 'delete')
	 * );
	 * \endcode
	 * 
	 * @param $rte String
	 * @param $maps Array
	 * @param $methods Array
	 */
	static function resources($rte, array $maps, array $methods=array())
	{
		$maps    = array_merge(self::$default_maps,    $maps);
		$methods = array_merge(self::$default_methods, $methods);
		foreach($methods as $method => $action)
		{
			$maps[':method'] = $method;
			$maps[':action'] = $action;
			self::connect($rte, $maps);
		}
	}
	
	/**
	 * Routes an URL.
	 * 
	 * @return Array
	 * @param $url String
	 * @param $method String[optional] HTTP METHOD
	 */
	static function & route_url($url, $method='GET')
	{
#		var_dump(self::$routes);
		
		$url = strtoupper($method).' '.trim($url);
		foreach(self::$routes as $rte => $maps)
		{
			# URL matches route?
			if (preg_match('#^'.$maps['regexp'].'$#', $url, $match))
			{
				$maps = array_merge(self::$default_maps, $maps);
				$args = preg_split(':[\s/.?]:', $rte, 0, PREG_SPLIT_NO_EMPTY);
				
				# removes unsignificant args
				foreach($args as $i => $arg)
				{
					if ($i >0 and strpos($arg, ':') !== 0) {
						unset($args[$i]);
					}
				}
				$args = array_values($args);
				
				# extracts controller, action and params
				foreach($args as $i => $arg)
				{
					if (strpos($arg, ':') === 0)
					{
						# controller, action, format or method
						if (array_key_exists($arg, self::$default_maps) and $arg != ':params') {
							$maps[$arg] = $match[$i+1];
						}
						
						# global params
						elseif ($arg == ':params')
						{
							#$maps[':params'] = array_merge($maps[':params'], explode('/', $match[$i+1]));
							$params = explode('/', $match[$i+1]);
							foreach($params as $p) {
								$maps[':params'][] = $p;
							}
						}
						
						# named param
						else {
							$maps[':params'][$arg] = $match[$i+1];
						}
					}
				}
				
				$maps[':method'] = $method;
				
				unset($maps['regexp']);
				unset($maps['requirements']);
				return $maps;
			}
		}
		
		trigger_error("No route for '$url'", E_USER_ERROR);
	}
	
	/**
	 * Transforms a route to a regular expression.
	 * 
	 * @return String
	 * @param $rte String
	 * @param $requirements Array[optional]
	 */
	private static function transform_route($rte, array $requirements=null)
	{
		# protects special chars
		$rte = str_replace('.', '\.', $rte);
		
		# handles special formats (eg: :id => .*)
		if (!empty($requirements))
		{
			foreach($requirements as $param => $re) {
				$requirements[$param] = "($re)";
			}
			$rte = str_replace(array_keys($requirements), array_values($requirements), $rte);
		}
		
		# generic transforms
		$rte = preg_replace(array_keys(self::$transforms), array_values(self::$transforms), $rte);
		
		return $rte;
	}
}

?>
